﻿////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <copyright>Copyright 2008-2011 Andreas Huber Doenni</copyright>
// Distributed under the Microsoft Public License (Ms-PL).
// See accompanying file License.txt or copy at http://mews.codeplex.com/license.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

namespace Mews.Mime
{
    using System;
    using System.Diagnostics.CodeAnalysis;
    using System.Globalization;
    using System.IO;
    using System.Text;

    /// <summary>Processes an entity.</summary>
    /// <typeparam name="TEntity">An <see cref="Entity"/> subclass.</typeparam>
    /// <remarks>
    /// <para><b>Implementation Design Goals</b>: The main design goals for the message parser are as follows:
    /// <list type="number">
    /// <item>Good maitainability</item>
    /// <item>Fast execution</item>
    /// <item>Low memory usage and low garbage production</item>
    /// </list>
    /// Of course, these goals somewhat contradict each other, which is why the above list is numbered. Points with a
    /// lower number always have priority over the ones with a higher one. The next paragraph highlights how these goals
    /// are pursued.</para>
    /// <para><b>Design Choices</b>: The choices that led to the current implementation somewhat follow from the goals
    /// stated above:
    /// <list type="bullet">
    /// <item><b>Modularization vs. performance:</b> The processing of a MIME entity is split over several classes. For
    /// example, an object of this class delegates the parsing work to a <see cref="HeaderProcessor{T}"/> object and an
    /// appropriate <see cref="ILineProcessor{T}"/> implementation object. If necessary, these in turn themselves
    /// further delegate the parsing to other classes. For pretty much each publicly visible class (e.g.
    /// <see cref="Message"/>) there is an internal processor class (e.g. <see cref="MessageProcessor"/>) that parses
    /// the incoming information and creates an instance of the public sibling class upon request. This arrangement
    /// allows for pretty good maintainability but in combination with the state machine approach (see below) also
    /// requires quite a few calls for the processing of each incoming piece of information. This is of minor importance
    /// when the piece consists of a full line of the message (that is, when a line of the message is processed at
    /// once). However, it can become a performance bottleneck when only a single byte is processed at once, as it is
    /// currently done for message headers. So, header processing could certainly be sped up quite a bit if more
    /// information than just a single byte is passed with each call, see <see cref="HeaderProcessor{T}"/> remarks for
    /// more information.</item>
    /// <item><b>Parsing approach:</b> After a few failed attempts, recursive descent (RD) parsing has been abandoned
    /// and replaced with a state machine approach. The main reason is that I've failed to achieve sufficient
    /// modularization and maintainability with RD. That's because RD seemingly requires that the input is tokenized
    /// before it can be parsed, that is, the whole task of picking apart a message is split into two phases. So a class
    /// like e.g. <see cref="ParameterizedHeaderFieldBodyProcessor"/> would need to contain code for both phases.
    /// However, in the tokenization phase we're not supposed to know that we're tokenizing a parameterized header
    /// field, as that would run against the nature of a tokenizer. The current approach on the other hand certainly
    /// allows for pretty good modularization and maintainability but is probably slow in headers (see above).</item>
    /// <item><b>Event granularity:</b> Most processing classes implement
    /// <see cref="ILineProcessor{T}.ProcessFirstLine">ILineProcessor&lt;T&gt;.ProcessFirstLine</see>, which accepts an
    /// <see cref="ArraySegment{T}"/> argument. Note that an <see cref="ArraySegment{T}"/> argument will usually contain
    /// multiple lines of a message and it is the duty of all methods accepting such arguments to process only exactly
    /// the first line and then return the offset to the next line. This way, almost all of the bytes that make up a
    /// message are only being looked at exactly once. Defining that any call to such a method will always pass an array
    /// segment that starts with a "\r\n" sequence was found to be the best way to handle multipart boundaries
    /// (according to <a href="http://www.ietf.org/rfc/rfc2046.txt">RFC 2046</a> 5.1.1./6 the "\r\n" sequence preceding
    /// a boundary line is formally part of the boundary and not the body part before the boundary). However, this also
    /// means that all other processors must be implemented to expect (and usually ignore) the first "\r\n" sequence
    /// handed to them, see <see cref="UnstructuredContentProcessor"/> for an example.</item>
    /// <item><b>Parsing termination:</b> Parsers usually detect that a structure has been fully parsed and then return
    /// to the caller and do not attempt to consume more data from the stream. This is not normally possible with MIME
    /// messages, as entities often don't have a termination marker. Even if such a marker exists (e.g. in the form of a
    /// multipart boundary delimiter) then the parser is <b>required</b> to still work on a best effort basis if the
    /// delimiter has been lost (see <a href="http://www.ietf.org/rfc/rfc2046.txt">RFC 2046</a> 5.1.2.). These
    /// requirements can easily be met with the following measures:
    /// <list type="bullet">
    /// <item>We define that a (child)parser never terminates on its own. Instead, it is always the parent parser (or
    /// the topmost <see cref="Message.Parse(Stream)"/> function) that tells its child parser when it has received all
    /// available data (which is done by calling <see cref="ILineProcessor{T}.GetResult"/>).</item>
    /// <item>Due to the state machine parsing approach, all parsers always return to the topmost parsing function after
    /// processing each line. This way, each parser always has the chance to inspect every line for which it delegates
    /// to a child parser. The processor object for an outer multipart entity is therefore able to detect one of its own
    /// boundaries even if the processor of an inner multipart entity is still waiting to see its closing boundary
    /// delimiter.</item>
    /// </list></item>
    /// <item><b>Memory management:</b> Modifying a method to produce less garbage <b>usually</b> also makes the method
    /// faster. Therefore, in general, most code tries to avoid producing garbage. For example, the processors that
    /// process a full line of input with each call accept an <see cref="ArraySegment{T}"/> object rather than a byte
    /// array as argument. Since the former represents just a segment of the buffer that is used to read the data from
    /// the stream, zero garbage is produced during line separation. Of course, the latter would produce one garbage
    /// byte array per line of input. Exceptions include the various switch statements that need to case-insensitively
    /// switch on a string. A few informal tests have shown that it is faster to first create an all upper-case variant
    /// of the string and then switch on that rather than to use the original string as a key into a dictionary with a
    /// case-insensitive comparer.</item>
    /// </list></para>
    /// </remarks>
    internal abstract class EntityProcessor<TEntity> : StateMachine<ArraySegment<byte>, int>
        where TEntity : Entity<TEntity>, new()
    {
        private static readonly string lowercaseInline = ToLower(DispositionKind.Inline.ToString());
        private static readonly string lowercaseFlowed = ToLower(Format.Flowed.ToString());
        private static readonly string lowercaseYes = ToLower(DelSP.Yes.ToString());
        private readonly HeaderProcessor<TEntity> headerProcessor = new HeaderProcessor<TEntity>();
        private TEntity result;
        private ILineProcessor<Body> bodyProcessor;

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>Gets the processed entity.</summary>
        /// <exception cref="InvalidOperationException"><see cref="GetResult"/> has been called before (it may only
        /// be called once).</exception>
        /// <remarks>The body of the entity is <c>null</c> if processing never got to the body.</remarks>
        public TEntity GetResult()
        {
            this.Result.Body = this.bodyProcessor == null ? null : this.bodyProcessor.GetResult();
            return this.Result;
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        internal EntityProcessor()
        {
            this.State = this.InHeader;
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>Creates and returns the default processor that is used in the absence of a Content-Type field.
        /// </summary>
        protected virtual ILineProcessor<Body> CreateDefaultBodyProcessor(
            EncodingTransformation encodingTransformation, Disposition disposition)
        {
            return new TextBodyProcessor(encodingTransformation, Encoding.ASCII, disposition, null);
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>Gets the processed header.</summary>
        /// <value>The header fields or <c>null</c> if the header was not fully processed.</value>
        private TEntity Result
        {
            get
            {
                if (this.result == null)
                {
                    this.result = this.headerProcessor.GetResult();
                }

                return this.result;
            }
        }

        private int InHeader(ArraySegment<byte> ev)
        {
            int newOffset = this.headerProcessor.ProcessFirstLine(ev);

            if ((newOffset - ev.Offset == 2) && (ev.Count > 2) && (ev.Array[ev.Offset] == (byte)'\r'))
            {
                // The fact that only two bytes have been processed and we're not at the end of the buffer means that
                // we're now on a "\r\n" sequence that immediately follows the last one --> we're at the end of the
                // header.
                // At this point it is not yet clear that a body will follow. It could just as well be a boundary (which
                // per RFC starts with a "\r\n" sequence). We therefore need to postpone the creation of the body
                // processor until we receive another line.
                this.State = this.InBody;
            }

            return newOffset;
        }

        private int InBody(ArraySegment<byte> ev)
        {
            this.bodyProcessor = GetBodyProcessor(
                this.Result.ContentTypeField,
                this.Result.ContentTransferEncodingField,
                GetDisposition(this.Result),
                this.CreateDefaultBodyProcessor);
            this.State = this.bodyProcessor.ProcessFirstLine;
            return this.State(ev);
        }

        private static ILineProcessor<Body> GetBodyProcessor(
            ParameterizedHeaderField contentType,
            ParameterizedHeaderField contentTransferEncoding,
            Disposition disposition,
            Func<EncodingTransformation, Disposition, ILineProcessor<Body>> createDefaultBodyProcessor)
        {
            // Absent transfer encoding means 7bit, see http://www.ietf.org/rfc/rfc2045.txt, 6.1./2.
            if (contentTransferEncoding != null)
            {
                var lowercaseContentTransferEncoding = ToLower(contentTransferEncoding.Value);

                switch (lowercaseContentTransferEncoding)
                {
                    case ContentTransferEncodingNames.SevenBit:
                    case ContentTransferEncodingNames.EightBit:
                    // "binary" is not currently legal for email and NNTP messages,
                    // see http://www.ietf.org/rfc/rfc2045.txt, 6.2./8.
                    // However, it is possible that such messages will apppear in the future.
                    case ContentTransferEncodingNames.Binary:
                        break;
                    case ContentTransferEncodingNames.QuotedPrintable:
                        return GetBodyProcessor(
                            contentType,
                            disposition,
                            createDefaultBodyProcessor,
                            EncodingTransformation.QuotedPrintable);
                    case ContentTransferEncodingNames.Base64:
                        return GetBodyProcessor(
                            contentType, disposition, createDefaultBodyProcessor, EncodingTransformation.Base64);
                    default:
                        FallbackReason fallbackReason = new FallbackReason(
                            FallbackReasonKind.UnrecognizedContentTransferEncoding, lowercaseContentTransferEncoding);
                        return new ApplicationBodyProcessor(
                            EncodingTransformation.Identity, disposition, fallbackReason);
                }
            }

            return GetBodyProcessor(
                contentType, disposition, createDefaultBodyProcessor, EncodingTransformation.Identity);
        }

        private static ILineProcessor<Body> GetBodyProcessor(
            ParameterizedHeaderField contentType,
            Disposition disposition,
            Func<EncodingTransformation, Disposition, ILineProcessor<Body>> createDefaultBodyProcessor,
            EncodingTransformation encodingTransformation)
        {
            if (contentType == null)
            {
                return createDefaultBodyProcessor(encodingTransformation, disposition);
            }

            var mediaTypes = ToLower(contentType.Value).Split('/');

            // We want to ignore subsubtypes and treat the absence of a subtype as unrecognized subtype
            Array.Resize(ref mediaTypes, 2);
            FallbackReason fallbackReason;

            switch (mediaTypes[0])
            {
                case MediaTypeNames.Application:
                    return new ApplicationBodyProcessor(encodingTransformation, mediaTypes[1], disposition);
                case MediaTypeNames.Image:
                    return new ImageBodyProcessor(encodingTransformation, mediaTypes[1], disposition);
                case MediaTypeNames.Message:
                    if (mediaTypes[1] == MessageSubtypeNames.Rfc822)
                    {
                        return new MessageBodyProcessor(mediaTypes[1], disposition);
                    }

                    fallbackReason = new FallbackReason(FallbackReasonKind.UnrecognizedMessageSubtype, mediaTypes[1]);
                    break;
                case MediaTypeNames.Multipart:
                    string boundary;

                    if (!contentType.Parameters.TryGetValue(ContentTypeParameterNames.Boundary, out boundary))
                    {
                        fallbackReason = new FallbackReason(FallbackReasonKind.MissingMultipartBoundaryParameter, null);
                        break;
                    }

                    switch (mediaTypes[1])
                    {
                        case MultipartSubtypeNames.Mixed:
                        case MultipartSubtypeNames.Alternative:
                        case MultipartSubtypeNames.Digest:
                        case MultipartSubtypeNames.Parallel:
                            return new MultipartBodyProcessor(mediaTypes[1], disposition, boundary);
                        default:
                            return new MultipartBodyProcessor(
                                disposition,
                                new FallbackReason(FallbackReasonKind.UnrecognizedMultipartSubtype, mediaTypes[1]),
                                boundary);
                    }

                case MediaTypeNames.Text:
                    string charset;
                    contentType.Parameters.TryGetValue(ContentTypeParameterNames.CharSet, out charset);
                    Encoding encoding = EncodingHelper.GetEncoding(charset);

                    if (encoding == null)
                    {
                        fallbackReason = new FallbackReason(FallbackReasonKind.UnrecognizedTextCharset, charset);
                        break;
                    }

                    switch (mediaTypes[1])
                    {
                        case TextSubtypeNames.Plain:
                            // Missing or unrecognized format values should be interpreted as "fixed", see
                            // http://www.ietf.org/rfc/rfc3676.txt, 4.
                            string rawFormat;
                            contentType.Parameters.TryGetValue(ContentTypeParameterNames.Format, out rawFormat);
                            var format = ToLower(rawFormat) == lowercaseFlowed ? Format.Flowed : Format.Fixed;

                            // Missing or unrecognized delsp values should be interpreted as "no", see
                            // http://www.ietf.org/rfc/rfc3676.txt, 4.
                            string rawDelsp;
                            contentType.Parameters.TryGetValue(ContentTypeParameterNames.DelSp, out rawDelsp);
                            bool delsp = ToLower(rawDelsp) == lowercaseYes;

                            return new TextBodyProcessor(
                                encodingTransformation, encoding, mediaTypes[1], disposition, format, delsp);
                        default:
                            return new TextBodyProcessor(
                                encodingTransformation,
                                encoding,
                                disposition,
                                new FallbackReason(FallbackReasonKind.UnrecognizedTextSubtype, mediaTypes[1]));
                    }

                default:
                    fallbackReason = new FallbackReason(FallbackReasonKind.UnrecognizedMediaType, mediaTypes[0]);
                    break;
            }

            return new ApplicationBodyProcessor(encodingTransformation, disposition, fallbackReason);
        }

        private static Disposition GetDisposition(Entity entity)
        {
            var contentDisposition = entity.ContentDispositionField;

            if (contentDisposition == null)
            {
                // See http://www.ietf.org/rfc/rfc2183.txt, 2.
                return null;
            }

            string filename;
            contentDisposition.Parameters.TryGetValue(ContentDispositionParameterNames.FileName, out filename);

            // Unrecognized disposition types should be treated as attachment, see
            // http://www.ietf.org/rfc/rfc2183.txt, 2.8.
            return new Disposition(
                ToLower(contentDisposition.Value) == lowercaseInline ? DispositionKind.Inline : DispositionKind.Attachment,
                filename);
        }

        [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "Pure ASCII, roundtrip normalization problems are impossible.")]
        private static string ToLower(string str)
        {
            return str == null ? null : str.ToLower(CultureInfo.InvariantCulture);
        }
    }
}
